package tp.badugi.net;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import tp.badugi.net.exceptions.IllegalCharactersException;
import tp.badugi.net.exceptions.IllegalParameterValueException;
import tp.badugi.net.exceptions.NameTakenException;
import tp.badugi.net.exceptions.NoSuchTableException;
import tp.badugi.net.exceptions.ParameterOutOfBoundsException;
import tp.badugi.net.exceptions.RequestTimedOutException;
import tp.badugi.net.exceptions.ServerCouldNotCompleteRequestException;
import tp.badugi.net.exceptions.TableFullException;
import static tp.badugi.net.ClientNetworkingChannel.*;

/**
 * This class provides a server side network communication API with the client.
 * 
 * @author bartek
 *
 */
public class ServerNetworkingChannel implements Runnable {
	
	static final boolean DEBUGGING_ON = true;
	private static final int PING_FREQUENCY = 10000;
	private static final int TIMEOUT_GRACE_PERIOD = 5000;
	
	public static final String DELIMITER = ";";
	public static final String PARAM_RETURN_RESULT = "PARAM_RETURN_RESULT";
	public static final String PARAM_EXCEPTION = "PARAM_EXCEPTION";
	public static final String PARAM_EXCEPTION_MESSAGE = "PARAM_EXCEPTION_MESSAGE";
	public static final String PARAM_POSSIBLE_ACTIONS = "PARAM_POSSIBLE_ACTIONS";
	public static final String PARAM_PLAYER_INDEX = "PARAM_PLAYER_NUMBER";
	public static final String PARAM_PLAYER_NAME = "PARAM_PLAYER_NAME";
	public static final String PARAM_PLAYER_NAMES = "PARAM_PLAYER_NAMES";
	public static final String PARAM_GAME_STATUS = "PARAM_GAME_STATUS";
	public static final String PARAM_BET_AMOUNT = "PARAM_BET_AMOUNT";
	public static final String PARAM_MIN_BET = "PARAM_MIN_BET";
	public static final String PARAM_TIMEOUT = "PARAM_TIMEOUT";
	public static final String PARAM_CURRENT_CARDS = "PARAM_CURRENT_CARDS";
	public static final String PARAM_TABLE_LIST = "PARAM_TABLE_LIST";
	public static final String PARAM_ACTION = "PARAM_ACTION";
	public static final String PARAM_AMOUNT = "PARAM_AMOUNT";
	public static final String PARAM_CARD_COUNT = "PARAM_CARD_COUNT";
	public static final String PARAM_BETTING_ROUND = "PARAM_BETTING_ROUND";
	public static final String PARAM_REVEALED_CARDS = "PARAM_REVEALED_CARDS";
	public static final String PARAM_WINNER_INDEXES = "PARAM_WINNER_INDEXES";
	public static final String PARAM_UPDATED_CHIP_COUNTS = "PARAM_UPDATED_CHIP_COUNTS";
	public static final String PARAM_CARDS = "PARAM_CARDS";
	public static final String PARAM_TIME_UNTIL_START = "PARAM_TIME_UNTIL_START";
	public static final String PARAM_TIME_UNTIL_RESTART = "PARAM_TIME_UNTIL_RESTART";
	public static final String ACTION_FOLD = "ACTION_FOLD";
	public static final String ACTION_CHECK = "ACTION_CHECK";
	public static final String ACTION_BET = "ACTION_BET";
	public static final String ACTION_CALL = "ACTION_CALL";
	public static final String ACTION_RAISE = "ACTION_RAISE";
	public static final String ACTION_ALLIN = "ACTION_ALLIN";
	public static final String REQUEST_ACTION = "REQUEST_ACTION";
	public static final String REQUEST_CARDS = "REQUEST_CARDS";
	public static final String RESPONSE_CREATE_TABLE = "RESPONSE_CREATE_TABLE";
	public static final String RESPONSE_JOIN_TABLE = "RESPONSE_JOIN_TABLE";
	public static final String RESPONSE_LIST_TABLES = "RESPONSE_LIST_TABLES";
	public static final String RESPONSE_LEAVE_TABLE = "RESPONSE_LEAVE_TABLE";
	public static final String NOTIFY_CARDS_RECEIVED = "NOTIFY_CARDS_RECEIVED";
	public static final String NOTIFY_BETTING_ROUND_CHANGED = "NOTIFY_BETTING_ROUND_CHANGED";
	public static final String NOTIFY_PLAYER_DISCONNECTED = "NOTIFY_PLAYER_DISCONNECTED";
	public static final String NOTIFY_PLAYER_IN_ACTION_CHANGED = "NOTIFY_PLAYER_IN_ACTION_CHANGED";
	public static final String NOTIFY_PLAYER_JOINED = "NOTIFY_PLAYER_JOINED";
	public static final String NOTIFY_PLAYER_LEFT = "NOTIFY_PLAYER_LEFT";
	public static final String NOTIFY_PLAYER_PERFORMED_ACTION = "NOTIFY_PLAYER_PERFORMED_ACTION";
	public static final String NOTIFY_PLAYER_RECONNECTED = "NOTIFY_PLAYER_RECONNECTED";
	public static final String NOTIFY_PLAYER_REPLACED_CARDS = "NOTIFY_PLAYER_REPLACED_CARDS";
	public static final String NOTIFY_PLAYER_ON_BUTTON_CHANGED = "NOTIFY_PLAYER_ON_BUTTON_CHANGED";
	public static final String NOTIFY_WINNER_REVEALED = "NOTIFY_WINNER_REVEALED";
	public static final String NOTIFY_GAME_ABOUT_TO_START = "NOTIFY_GAME_ABOUT_TO_START";
	public static final String NOTIFY_GAME_ABOUT_TO_RESTART = "NOTIFY_GAME_ABOUT_TO_RESTART";
	public static final String PING_PROBE = "PING_PROBE";
	public static final String ERROR_MSG_TABLE_NAME_ILLEGAL_CHARACTERS = 
			"Table name contains illegal characters.";
	public static final String ERROR_MSG_PLAYER_NAME_ILLEGAL_CHARACTERS = 
			"Player name contains illegal characters.";
	public static final String ERROR_MSG_PREPARING_REQUEST = "Error preparing request.";
	public static final String ERROR_MSG_PREPARING_RESPONSE = "Error preparing response.";		
	
	private iServerRequestListener serverListener;
	private iTableRequestListener tableListener;
	private Socket socket;
	private PrintWriter outputBuffer;
	private BufferedReader inputBuffer;
	private ActionRequestResponse actionRequestResponse;
	private String[] cardsRequestResponse;
	private boolean responsiveToPing = true;

	public ServerNetworkingChannel(iServerRequestListener serverRequestListener, Socket socket) {
		this.serverListener = serverRequestListener;
		this.socket = socket;
	}

	@Override
	public void run() {
		try {
			outputBuffer = new PrintWriter(socket.getOutputStream());
			inputBuffer = new BufferedReader(
					new InputStreamReader(socket.getInputStream()));
			
			while (!Thread.interrupted()) {
				try {
					String message = inputBuffer.readLine();
					if (message == null) {
						throw new IOException("Unable to read from socket.");
					}
					if (DEBUGGING_ON) {
						String time = getTime();
						System.out.println(time + ": Server received message: " + message);
					}
					String[] messageBlock = message.split(DELIMITER);
					String request = messageBlock[0];
					JSONObject parameters;
					if (messageBlock.length > 1) {
						parameters = new JSONObject(messageBlock[1]);
					}
					else {
						parameters = null;
					}
					switch (request) {
					case REQUEST_CREATE_TABLE:
						handleCreateTableRequest(parameters);
						break;
					case REQUEST_JOIN_TABLE:
						handleJoinTableRequest(parameters);
						break;
					case REQUEST_LEAVE_TABLE:
						handleLeaveTableRequest(parameters);
						break;
					case REQUEST_LIST_TABLES:
						handleListTablesRequest(parameters);
						break;
					case RESPONSE_CHOSEN_ACTION:
						handleActionChosenResponse(parameters);
						break;
					case RESPONSE_CHOSEN_CARDS:
						handleCardsChosenResponse(parameters);
						break;
					case NOTIFY_CONNECTION_CLOSE:
						handleDisconnect();
						Thread.currentThread().interrupt();
						break;
					case PING_RESPONSE:
						handlePingResponse();
						break;
					default:
						throw new IllegalArgumentException(
								"Don't know how to handle request: " + request);
					}
				}
				catch (JSONException | IllegalArgumentException e) {
					System.err.println("Packet not properly formatted on client side.");
					e.printStackTrace(); // TODO Remove when finished testing
				}
			}
		} catch (IOException e) {
			System.err.println("Connection lost with client: " + e.getMessage());
			handleDisconnect();
		}
	}
	
	private void handleDisconnect() {
		if (tableListener != null) {
			tableListener.leaveTable();
		}
	}

	private void handlePingResponse() {
		responsiveToPing = true;
	}

	private void sendPingProbe() {
		responsiveToPing = false;
		String requestMessage = PING_PROBE;
		sendMessage(requestMessage);
	}

	public synchronized ActionRequestResponse requestAction(
			String[] possibleActions, 
			long betAmount, 
			long minBet, 
			int timeout
	) throws RequestTimedOutException {	
		// Try to prepare JSON formatted request
		try {
			JSONObject jsonEncodedParameters = new JSONObject();
			jsonEncodedParameters.put(PARAM_POSSIBLE_ACTIONS, possibleActions);
			jsonEncodedParameters.put(PARAM_BET_AMOUNT, betAmount);
			jsonEncodedParameters.put(PARAM_MIN_BET, minBet);
			jsonEncodedParameters.put(PARAM_TIMEOUT, timeout);
			String jsonString = jsonEncodedParameters.toString();
			String requestMessage = REQUEST_ACTION + DELIMITER + jsonString;
			
			// Synchronize threads in order to exchange request and response
			synchronized (this) {
				// Flush previous response and send request
				actionRequestResponse = null;
				sendMessage(requestMessage);
	
				// Wait for response 
				long timeToWait = timeout + TIMEOUT_GRACE_PERIOD;
				long timeAtStart = System.currentTimeMillis();
				while (actionRequestResponse == null) {
					try {
						wait(timeToWait);
						timeToWait -= System.currentTimeMillis() - timeAtStart;
						if (timeToWait <= 0) {
							throw new RequestTimedOutException("The client did not respond.");
						}
					}
					catch (InterruptedException e) {
						String message = "Interrupted while waiting for response from client.";
						throw new RequestTimedOutException(message);
					}
				}
				
				// Return result
				return actionRequestResponse;
			}
		}
		
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
			return new ActionRequestResponse(ACTION_FOLD, 0);
		}
	}
	
	public synchronized String[] requestCards(String[] currentCards, int timeout) 
			throws RequestTimedOutException 
	{
		// Try to prepare JSON formatted request
		try {
			JSONObject jsonEncodedParameters = new JSONObject();
			jsonEncodedParameters.put(PARAM_CURRENT_CARDS, currentCards);
			jsonEncodedParameters.put(PARAM_TIMEOUT, timeout);
			String jsonString = jsonEncodedParameters.toString();
			String requestMessage = REQUEST_CARDS + DELIMITER + jsonString;
			
			// Synchronize threads in order to exchange request and response
			synchronized (this) {
				// Flush previous response and send request
				cardsRequestResponse = null;
				sendMessage(requestMessage);
				
				// Wait for response 
				long timeToWait = timeout + TIMEOUT_GRACE_PERIOD;
				long timeAtStart = System.currentTimeMillis();
				while (cardsRequestResponse == null) {
					try {
						wait(timeToWait);
						timeToWait -= System.currentTimeMillis() - timeAtStart;
						if (timeToWait <= 0) {
							throw new RequestTimedOutException("The client did not respond.");
						}
					}
					catch (InterruptedException e) {
						String message = "Interrupted while waiting for response from client.";
						throw new RequestTimedOutException(message);
					}
				}
				
				if (cardsRequestResponse.length == 0) {
					cardsRequestResponse = null;
				}
				
				// Return result
				return cardsRequestResponse;
			}
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
			return null;
		}
	}
	
	public synchronized void notifyCardsReceived(String[] cards) {
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_CARDS, cards);
			String request = NOTIFY_CARDS_RECEIVED;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyPlayerJoined(int playerIndex, GameStatus gameStatus) 
	{
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_PLAYER_INDEX, playerIndex);
			parameters.put(PARAM_GAME_STATUS, gameStatus.toJSONObject());
			String request = NOTIFY_PLAYER_JOINED;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyPlayerLeft(String playerName, GameStatus gameStatus) 
	{
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_PLAYER_NAME, playerName);
			parameters.put(PARAM_GAME_STATUS, gameStatus.toJSONObject());
			String request = NOTIFY_PLAYER_LEFT;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyPlayerDisconnected(String playerName) 
	{
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_PLAYER_NAME, playerName);
			String request = NOTIFY_PLAYER_DISCONNECTED;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyPlayerReconnected(String playerName) 
	{
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_PLAYER_NAME, playerName);
			String request = NOTIFY_PLAYER_RECONNECTED;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyPlayerInActionChanged(int playerIndex, String[] playerNames) 
	{
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_PLAYER_INDEX, playerIndex);
			parameters.put(PARAM_PLAYER_NAMES, playerNames);
			String request = NOTIFY_PLAYER_IN_ACTION_CHANGED;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyPlayerWithButtonChanged(int playerIndex, String[] playerNames) 
	{
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_PLAYER_INDEX, playerIndex);
			parameters.put(PARAM_PLAYER_NAMES, playerNames);
			String request = NOTIFY_PLAYER_ON_BUTTON_CHANGED;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyPlayerPerformedAction(
			int playerIndex,
			String action, 
			long amount,
			GameStatus gameStatus) 
	{
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_PLAYER_INDEX, playerIndex);
			parameters.put(PARAM_ACTION, action);
			parameters.put(PARAM_AMOUNT, amount);
			parameters.put(PARAM_GAME_STATUS, gameStatus.toJSONObject());
			
			String request = NOTIFY_PLAYER_PERFORMED_ACTION;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyPlayerReplacedCards(String playerName, int cardCount) {
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_PLAYER_NAME, playerName);
			parameters.put(PARAM_CARD_COUNT, cardCount);
			
			String request = NOTIFY_PLAYER_REPLACED_CARDS;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyBettingRoundChanged(int bettingRound) {
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_BETTING_ROUND, bettingRound);
			
			String request = NOTIFY_BETTING_ROUND_CHANGED;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public synchronized void notifyWinnerRevealed(
			String[][] revealedCards,
			int[] winnerIndexes, 
			String[] playerNames, 
			long[] updatedChipCounts) 
	{
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_REVEALED_CARDS, revealedCards);
			parameters.put(PARAM_WINNER_INDEXES, winnerIndexes);
			parameters.put(PARAM_PLAYER_NAMES, playerNames);
			parameters.put(PARAM_UPDATED_CHIP_COUNTS, updatedChipCounts);
			
			String request = NOTIFY_WINNER_REVEALED;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}

	private void handleActionChosenResponse(JSONObject parameters) throws JSONException {
		// Decode JSON encoded response
		String action = null;
		long amount = 0;
		if (!parameters.isNull(PARAM_ACTION)) {
			action = parameters.getString(PARAM_ACTION);
		}
		if (!parameters.isNull(PARAM_AMOUNT)) {
			amount = parameters.getLong(PARAM_AMOUNT);
		}
		ActionRequestResponse response = new ActionRequestResponse(action, amount);
		
		// Synchronize threads in order to exchange request and response
		synchronized (this) {
			// Set response
			actionRequestResponse = response;
			notifyAll();
		}
	}
	
	private void handleCardsChosenResponse(JSONObject parameters) throws JSONException {
		// Decode JSON encoded response
		String[] chosenCards = null;
		if (!parameters.isNull(PARAM_CHOSEN_CARDS)) {
			JSONArray jsonArray = parameters.getJSONArray(PARAM_CHOSEN_CARDS);
			chosenCards = new String[jsonArray.length()];
			for (int i = 0; i < jsonArray.length(); i++) {
				chosenCards[i] = jsonArray.getString(i);
			}
		}
		
		// Synchronize threads in order to exchange request and response
		synchronized (this) {
			// Set response
			cardsRequestResponse = chosenCards;
			notifyAll();
		}
	}

	private void handleListTablesRequest(JSONObject parameters) {
		// Only proceed if we are not already at a table
		if (tableListener == null) {
			// Get table list
			String[] tableList = serverListener.getTableNamesList();
			
			// Prepare and send response
			try {
				JSONObject jsonEncodedResponse = new JSONObject();
				JSONArray jsonEncodedTableList = new JSONArray(tableList);
				jsonEncodedResponse.put(PARAM_RETURN_RESULT, true);
				jsonEncodedResponse.put(PARAM_TABLE_LIST, jsonEncodedTableList);
				String jsonString = jsonEncodedResponse.toString();
				String responseMessage = RESPONSE_LIST_TABLES + DELIMITER + jsonString;
				sendMessage(responseMessage);
			} 
			catch (JSONException e) {
				System.err.println(ERROR_MSG_PREPARING_RESPONSE);
				e.printStackTrace();
			}
		}
		
		// Since we are at a table, prepare and send failure response
		else {
			try {
				JSONObject jsonEncodedResponse = new JSONObject();
				jsonEncodedResponse.put(PARAM_RETURN_RESULT, false);
				jsonEncodedResponse.put(PARAM_EXCEPTION, 
						new ServerCouldNotCompleteRequestException().getClass().getName());
				jsonEncodedResponse.put(PARAM_EXCEPTION_MESSAGE, "Leave table first.");
				String jsonString = jsonEncodedResponse.toString();
				String responseMessage = RESPONSE_LIST_TABLES + DELIMITER + jsonString;
				sendMessage(responseMessage);
			} 
			catch (JSONException e) {
				System.err.println(ERROR_MSG_PREPARING_RESPONSE);
				e.printStackTrace();
			}
		}
	}

	private void handleLeaveTableRequest(JSONObject parameters) {
		// Only proceed if we are currently at a table
		if (tableListener != null) {
			// Leave table
			tableListener.leaveTable();
			tableListener = null;
			
			// Prepare and send success response
			try {
				JSONObject jsonEncodedResponse = new JSONObject();
				jsonEncodedResponse.put(PARAM_RETURN_RESULT, true);
				String jsonString = jsonEncodedResponse.toString();
				String responseMessage = RESPONSE_LEAVE_TABLE + DELIMITER + jsonString;
				sendMessage(responseMessage);
			} 
			catch (JSONException e) {
				System.err.println(ERROR_MSG_PREPARING_RESPONSE);
				e.printStackTrace();
			}
		}
		
		// Since we are not at a table, prepare and send failure response
		else {
			try {
				JSONObject jsonEncodedResponse = new JSONObject();
				jsonEncodedResponse.put(PARAM_RETURN_RESULT, false);
				jsonEncodedResponse.put(PARAM_EXCEPTION, 
						new ServerCouldNotCompleteRequestException().getClass().getName());
				jsonEncodedResponse.put(PARAM_EXCEPTION_MESSAGE, "Not connected to a table.");
				String jsonString = jsonEncodedResponse.toString();
				String responseMessage = RESPONSE_LEAVE_TABLE + DELIMITER + jsonString;
				sendMessage(responseMessage);
			} 
			catch (JSONException e) {
				System.err.println(ERROR_MSG_PREPARING_RESPONSE);
				e.printStackTrace();
			}
		}
	}

	private synchronized void handleJoinTableRequest(JSONObject parameters) throws JSONException {
		// Only proceed if we are not already at a table
		if (tableListener == null) {
			// Decode JSON encoded parameters
			String tableName = "";
			String myPlayerName = "";
			if (!parameters.isNull(PARAM_TABLE_NAME)) {
				tableName = parameters.getString(PARAM_TABLE_NAME);
			}
			if (!parameters.isNull(PARAM_MY_PLAYER_NAME)) {
				myPlayerName = parameters.getString(PARAM_MY_PLAYER_NAME);
			}
			
			// Try to join the table and prepare success response message
			JSONObject jsonEncodedResponse = new JSONObject();
			try {
				try {
					tableListener = serverListener.joinTable(this, tableName, myPlayerName);
					jsonEncodedResponse.put(PARAM_RETURN_RESULT, true);
					GameStatus gameStatus = tableListener.getGameStatus();
					jsonEncodedResponse.put(PARAM_GAME_STATUS, gameStatus.toJSONObject());
				} 
				
				// On failure prepare response message about encountered error
				catch (NoSuchTableException | TableFullException | NameTakenException |
						IllegalCharactersException | ServerCouldNotCompleteRequestException e) 
				{
					jsonEncodedResponse.put(PARAM_RETURN_RESULT, false);
					jsonEncodedResponse.put(PARAM_EXCEPTION, e.getClass().getName());
					jsonEncodedResponse.put(PARAM_EXCEPTION_MESSAGE, e.getMessage());
				}
				
				// Send prepared message to client
				finally {
					String jsonString = jsonEncodedResponse.toString();
					String responseMessage = RESPONSE_JOIN_TABLE + DELIMITER + jsonString;
					sendMessage(responseMessage);
				}
			}
			catch (JSONException e) {
				System.err.println(ERROR_MSG_PREPARING_RESPONSE);
				e.printStackTrace();
			}
		}
		
		// Since we are already at a table, prepare and send failure response
		else {
			try {
				JSONObject jsonEncodedResponse = new JSONObject();
				jsonEncodedResponse.put(PARAM_RETURN_RESULT, false);
				jsonEncodedResponse.put(PARAM_EXCEPTION, 
						new ServerCouldNotCompleteRequestException().getClass().getName());
				jsonEncodedResponse.put(PARAM_EXCEPTION_MESSAGE, "Leave table first.");
				String jsonString = jsonEncodedResponse.toString();
				String responseMessage = RESPONSE_JOIN_TABLE + DELIMITER + jsonString;
				sendMessage(responseMessage);
			} 
			catch (JSONException e) {
				System.err.println(ERROR_MSG_PREPARING_RESPONSE);
				e.printStackTrace();
			}
		}
	}

	private synchronized void handleCreateTableRequest(JSONObject parameters) throws JSONException {
		// Only proceed if we are not already at a table
		if (tableListener == null) {
			// Decode JSON encoded parameters
			String myPlayerName = "";
			String tableName = "";
			int maxPlayers = 0;
			long startingChips = 0;
			long smallBlind = 0;
			long bigBlind = 0;
			if (!parameters.isNull(PARAM_MY_PLAYER_NAME)) {
				myPlayerName = parameters.getString(PARAM_MY_PLAYER_NAME);
			}
			if (!parameters.isNull(PARAM_TABLE_NAME)) {
				tableName = parameters.getString(PARAM_TABLE_NAME);
			}
			if (!parameters.isNull(PARAM_MAX_PLAYERS)) {
				maxPlayers = parameters.getInt(PARAM_MAX_PLAYERS);
			}
			if (!parameters.isNull(PARAM_STARTING_CHIPS)) {
				startingChips = parameters.getLong(PARAM_STARTING_CHIPS);
			}
			if (!parameters.isNull(PARAM_SMALL_BLIND)) {
				smallBlind = parameters.getLong(PARAM_SMALL_BLIND);
			}
			if (!parameters.isNull(PARAM_BIG_BLIND)) {
				bigBlind = parameters.getLong(PARAM_BIG_BLIND);
			}
			
			// Try to create a table and prepare success response message
			JSONObject jsonEncodedResponse = new JSONObject();
			try {
				try {
					tableListener = serverListener.createTable(this, myPlayerName, 
							tableName, maxPlayers, startingChips, smallBlind, bigBlind);
					jsonEncodedResponse.put(PARAM_RETURN_RESULT, true);
					GameStatus gameStatus = tableListener.getGameStatus();
					jsonEncodedResponse.put(PARAM_GAME_STATUS, gameStatus.toJSONObject());
				} 
				
				// On failure prepare response message about encountered error
				catch (NameTakenException | IllegalCharactersException | 
						IllegalParameterValueException | ServerCouldNotCompleteRequestException e) 
				{
					jsonEncodedResponse.put(PARAM_RETURN_RESULT, false);
					jsonEncodedResponse.put(PARAM_EXCEPTION, e.getClass().getName());
					jsonEncodedResponse.put(PARAM_EXCEPTION_MESSAGE, e.getMessage());
				}
				
				// Send prepared message to client
				finally {
					String jsonString = jsonEncodedResponse.toString();
					String responseMessage = RESPONSE_CREATE_TABLE + DELIMITER + jsonString;
					sendMessage(responseMessage);
				}
			}
			catch (JSONException e) {
				System.err.println(ERROR_MSG_PREPARING_RESPONSE);
				e.printStackTrace();
			}
		}
		
		// Since we are at a table, prepare and send failure response
		else {
			try {
				JSONObject jsonEncodedResponse = new JSONObject();
				jsonEncodedResponse.put(PARAM_RETURN_RESULT, false);
				jsonEncodedResponse.put(PARAM_EXCEPTION, 
						new ServerCouldNotCompleteRequestException().getClass().getName());
				jsonEncodedResponse.put(PARAM_EXCEPTION_MESSAGE, "Leave table first.");
				String jsonString = jsonEncodedResponse.toString();
				String responseMessage = RESPONSE_CREATE_TABLE + DELIMITER + jsonString;
				sendMessage(responseMessage);
			} 
			catch (JSONException e) {
				System.err.println(ERROR_MSG_PREPARING_RESPONSE);
				e.printStackTrace();
			}
		}
	}
	
	private synchronized void sendMessage(String formattedResponse) {
		outputBuffer.println(formattedResponse);
		outputBuffer.flush();
		if (DEBUGGING_ON) {
			String time = getTime();
			System.out.println(time + ": Server sent message: " + formattedResponse);
		}
	}
	
	private String getTime() {
		DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
		Calendar cal = Calendar.getInstance();
		String time = dateFormat.format(cal.getTime());
		return time;
	}

	public void notifyGameAboutToStart(int timeUntilStart, GameStatus gameStatus) {
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_TIME_UNTIL_START, timeUntilStart);
			parameters.put(PARAM_GAME_STATUS, gameStatus.toJSONObject());
			String request = NOTIFY_GAME_ABOUT_TO_START;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
	
	public void notifyGameAboutToRestart(int timeUntilRestart) {
		// Try to prepare JSON formatted request
		try {
			JSONObject parameters = new JSONObject();
			parameters.put(PARAM_TIME_UNTIL_RESTART, timeUntilRestart);
			String request = NOTIFY_GAME_ABOUT_TO_RESTART;
			String jsonParameters = parameters.toString();
			String notificationMessage = request + DELIMITER + jsonParameters;
			sendMessage(notificationMessage);
		}
		// Try to recover from request formatting error.
		catch (JSONException e) {
			System.err.println(ERROR_MSG_PREPARING_REQUEST);
			e.printStackTrace();
		}
	}
}
